﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace DynamicTestDemo
{
    /// <summary>
    /// DynamicTypeFactory class
    /// </summary>
    public class DynamicTypeFactory
    {
        #region DynamicBase class

        /// <summary>
        /// Base for dynamic class
        /// </summary>
        public class DynamicBase : INotifyPropertyChanged, IDisposable
        {
            private IDynamicPropertyProvider provider;

            public DynamicBase(IDynamicPropertyProvider provider)
            {
                this.provider = provider;

                if (provider != null) provider.ValueChanged += OnPropertyChanged;
            }

            protected object GetValue(string name) { return provider.GetPropertyValue(name); }

            protected void SetValue(string name, object value)
            {
                provider.SetPropertyValue(name, value);
            }

            private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                if (PropertyChanged != null) PropertyChanged(this, e);
            }

            #region INotifyPropertyChanged Members

            public event PropertyChangedEventHandler PropertyChanged;

            #endregion

            #region IDisposable Members

            public void Dispose()
            {
                if (provider != null) provider.ValueChanged -= OnPropertyChanged;
            }

            #endregion
        }

        #endregion

        /// <summary>
        /// Creates the type.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="properties">The properties.</param>
        /// <returns></returns>
        public static Type CreateType(string name, IEnumerable<string> properties)
        {
            // Define dynamic assembly
            var assemblyName = new AssemblyName("DynamicAssembly");

            var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            var module = assemblyBuilder.DefineDynamicModule("DynamicModule");

            // Define type
            var typeBuilder = module.DefineType(
                name,
                TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,
                typeof(DynamicBase));

            var baseConstructor = typeof(DynamicBase).GetConstructor(new[] { typeof(IDynamicPropertyProvider) });

            // Define constructor
            var constructor = typeBuilder.DefineConstructor(
                MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
                CallingConventions.Standard,
                new[] { typeof(IDynamicPropertyProvider) });

            var ilGen = constructor.GetILGenerator();
            ilGen.Emit(OpCodes.Ldarg_0);
            ilGen.Emit(OpCodes.Ldarg_1);
            ilGen.Emit(OpCodes.Call, baseConstructor);
            ilGen.Emit(OpCodes.Ret);

            // Prepare to define properties
            var getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
            var getMethodInfo = typeof(DynamicBase).GetMethod("GetValue", BindingFlags.NonPublic | BindingFlags.Instance);
            var setMethodInfo = typeof(DynamicBase).GetMethod("SetValue", BindingFlags.NonPublic | BindingFlags.Instance);

            foreach (var property in properties)
            {
                // Define property get method
                var getMethod = typeBuilder.DefineMethod("get_" + property, getSetAttr, typeof(object), Type.EmptyTypes);
                ilGen = getMethod.GetILGenerator();
                ilGen.Emit(OpCodes.Ldarg_0);
                ilGen.Emit(OpCodes.Ldstr, property);
                ilGen.Emit(OpCodes.Callvirt, getMethodInfo);
                ilGen.Emit(OpCodes.Ret);

                // Define property set method
                var setMethod = typeBuilder.DefineMethod("set_" + property, getSetAttr, null, new[] { typeof(object) });
                ilGen = setMethod.GetILGenerator();
                ilGen.Emit(OpCodes.Ldarg_0);
                ilGen.Emit(OpCodes.Ldstr, property);
                ilGen.Emit(OpCodes.Ldarg_1);
                ilGen.Emit(OpCodes.Callvirt, setMethodInfo);
                ilGen.Emit(OpCodes.Ret);

                // Define property
                var propertyBuilder = typeBuilder.DefineProperty(property, PropertyAttributes.HasDefault, typeof(object), Type.EmptyTypes);

                propertyBuilder.SetGetMethod(getMethod);
                propertyBuilder.SetSetMethod(setMethod);
            }

            // Materialize type
            return typeBuilder.CreateType();
        }
    }

    public interface IDynamicPropertyProvider
    {
        object GetPropertyValue(string name);
        void SetPropertyValue(string name, object value);
        event PropertyChangedEventHandler ValueChanged;
    }

    public class MyDynamicProvider : IDynamicPropertyProvider
    {
        public Dictionary<string, object> MyValues { get; private set; }

        public MyDynamicProvider()
        {
            MyValues = new Dictionary<string, object>();
        }

        public object CreateDynamicWrapper()
        {
            var type = DynamicTypeFactory.CreateType("MyWrapper", MyValues.Keys.ToArray());
            return Activator.CreateInstance(type, this);
        }

        public object GetPropertyValue(string name)
        {
            return MyValues[name];
        }

        public void SetPropertyValue(string name, object value)
        {
            MyValues[name] = value;
            if (ValueChanged != null) ValueChanged(this, new PropertyChangedEventArgs(name));
        }

        public event PropertyChangedEventHandler ValueChanged;
    }
}